//////////////////////////////////////////////////////////////////////////////////
//                                                                              //
//  This file is part of the buola project (https://code.google.com/p/buola/).  //
//                                                                              //
//  Copyright(c) 2007-2012 Xavi Gratal                                          //
//  gratal AT gmail DOT com                                                     //
//                                                                              //
//  Buola is free software: you can redistribute it and/or modify               //
//  it under the terms of the GNU General Public License as published by        //
//  the Free Software Foundation, either version 3 of the License, or           //
//  (at your option) any later version.                                         //
//                                                                              //
//  Buola is distributed in the hope that it will be useful,                    //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of              //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               //
//  GNU General Public License for more details.                                //
//                                                                              //
//  You should have received a copy of the GNU General Public License           //
//  along with buola.  If not, see <http://www.gnu.org/licenses/>.              //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////

#include <buola/xml/cdoc.h>
#include <buola/xml/cnode.h>
#include <buola/xml/lookup.h>
#include <buola/io/cfile.h>
#include <buola/io.h>

namespace buola { namespace xml {

CDoc::~CDoc()
{
}
    
bool CDoc::Load(const io::CURI &pURI)
{
    ///\todo do this more buola
    io::CFile lFile(pURI);
    std::size_t lFileSize=lFile.Size();

    io::PStream lS=io::open(pURI,io::EMode::READ);

    std::vector<char> lBuf(lFileSize);

    mText.resize(lFileSize+1,0);
    if(lS->Read(reinterpret_cast<uint8_t*>(&mText[0]),lFileSize).Count()!=lFileSize)
        throw XIO("can't read xml file");
    mText[lFileSize]=0;

    DoParse();
    
    return true;
}

bool CDoc::Save(const io::CURI &pURI)
{
    throw XNotImplemented("xml::CDoc::Save");
}

void CDoc::Parse(const std::string &pText)
{
    if(pText.empty())
        mText="\0";
    else if(pText.back()!='\0')
        mText=pText+'\0';
    else
        mText=pText;
    
    DoParse();
}

void CDoc::Parse(std::string &&pText)
{
    mText=std::move(pText);
    if(mText.empty()||mText.back()!='\0')
        mText+='\0';
    
    DoParse();
}

void CDoc::Parse(const char *pText,size_t pLength)
{
    mText=std::string(pText,pLength+1);
    
    DoParse();
}

void CDoc::Parse(const char *pText)
{
    Parse(pText,std::string::traits_type::length(pText)+1);
}

void CDoc::Create(const std::string &pRoot)
{
    Clear();
    SNode *lDeclaration=CreateNode(ENodeType::DECLARATION);
    lDeclaration->mAttributes.push_front(*CreateAttribute("version","1.0"));
    mDocNode->mChildren.push_back(*lDeclaration);
    mDocNode->mChildren.push_back(*CreateNode(ENodeType::ELEMENT,pRoot));
}

///Clears the document by deleting all nodes and clearing the memory pool.
///All nodes owned by document pool are destroyed.
void CDoc::Clear()
{
    mPool.Clear();
    mDocNode=new(mPool) SNode(this,ENodeType::DOCUMENT);
}

void CDoc::AddDocType(const std::string &pName,const std::string &pExternal,const std::string &pSystem)
{
    msg_warn() << "xml::CDoc::AddDocType not implemented\n";
}

std::string CDoc::ToString() const
{
    std::string lString;
    CNode(mDocNode).AppendTo(lString);
    return lString;
}

//node management
string_view CDoc::CreateString(const std::string &pString)
{
    if(pString.empty()) return string_view();
    char *lText=(char*)mPool.allocate(pString.length()*sizeof(char));
    memcpy(lText,pString.data(),pString.length()*sizeof(char));
    return string_view(lText,pString.length());
}

CDoc::SNode *CDoc::CreateNode(ENodeType pType,const std::string &pName,const std::string &pValue)
{
    return new(mPool) SNode(this,pType,CreateString(pName),CreateString(pValue));
}

CDoc::SAttribute *CDoc::CreateAttribute(const std::string &pName,const std::string &pValue)
{
    return new(mPool) SAttribute(CreateString(pName),CreateString(pValue));
}

///////////////////////////////////////////////////////////////////////
// Internal character utility functions

// Detect whitespace character
inline unsigned char CDoc::Test(char pChar,const unsigned char *pTable)
{
    return pTable[static_cast<unsigned char>(pChar)];
}

// Insert coded character, using UTF8 or 8-bit ASCII
inline void CDoc::InsertCodedCharacter(TIterator &pI,unsigned long pCode)
{
    if (mParseFlags & EParseFlags::NO_UTF8)
    {
        // Insert 8-bit ASCII character
        // Todo: possibly verify that code is less than 256 and use replacement char otherwise?
        pI[0] = static_cast<char>(pCode);
        ++pI;
    }
    else
    {
        // Insert UTF8 sequence
        if (pCode < 0x80)    // 1 byte sequence
        {
            pI[0] = static_cast<unsigned char>(pCode);
            pI+=1;
        }
        else if (pCode < 0x800)  // 2 byte sequence
        {
            pI[1] = static_cast<unsigned char>((pCode | 0x80) & 0xBF); pCode >>= 6;
            pI[0] = static_cast<unsigned char>(pCode | 0xC0);
            pI += 2;
        }
        else if (pCode < 0x10000)    // 3 byte sequence
        {
            pI[2] = static_cast<unsigned char>((pCode | 0x80) & 0xBF); pCode >>= 6;
            pI[1] = static_cast<unsigned char>((pCode | 0x80) & 0xBF); pCode >>= 6;
            pI[0] = static_cast<unsigned char>(pCode | 0xE0);
            pI += 3;
        }
        else if (pCode < 0x110000)   // 4 byte sequence
        {
            pI[3] = static_cast<unsigned char>((pCode | 0x80) & 0xBF); pCode >>= 6;
            pI[2] = static_cast<unsigned char>((pCode | 0x80) & 0xBF); pCode >>= 6;
            pI[1] = static_cast<unsigned char>((pCode | 0x80) & 0xBF); pCode >>= 6;
            pI[0] = static_cast<unsigned char>(pCode | 0xF0);
            pI += 4;
        }
        else    // Invalid, only codes up to 0x10FFFF are allowed in Unicode
        {
            throw XParse("invalid numeric character entity", pI);
        }
    }
}

inline void CDoc::Skip(TIterator &pI,const unsigned char *pTable)
{
    while(Test(*pI,pTable)) ++pI;
}

inline void CDoc::SkipUntil(TIterator &pI,char p1)
{
    while(pI[0]!=p1)
    {
        if (!pI[0])
            throw XParse("unexpected end of data",pI);
        ++pI;
    }
}

inline void CDoc::SkipUntil(TIterator &pI,char p1,char p2)
{
    while(pI[0]!=p1||pI[1]!=p2)
    {
        if (!pI[0])
            throw XParse("unexpected end of data",pI);
        ++pI;
    }
}

inline void CDoc::SkipUntil(TIterator &pI,char p1,char p2,char p3)
{
    while(pI[0]!=p1||pI[1]!=p2||pI[2]!=p3)
    {
        if (!pI[0])
            throw XParse("unexpected end of data",pI);
        ++pI;
    }
}

inline void CDoc::Expect(TIterator &pI,const unsigned char *pTable)
{
    if(!Test(*pI,pTable))
        throw XParse("table expect failed",pI);
    ++pI;
}

inline void CDoc::Expect(TIterator &pI,char pChar)
{
    if(*pI!=pChar)
        throw XParse("expect failed",pI);
    ++pI;
}

inline void CDoc::Expect2(TIterator &pI,char p1,char p2)
{
    Expect(pI,p1); 
    Expect(pI,p2);
}

inline char CDoc::ExpectOneOf(TIterator &pI,char p1,char p2)
{
    char lChar=*pI;
    if(lChar!=p1&&lChar!=p2)
        throw XParse("expect failed",pI);
    ++pI;
    return lChar;
}

// Skip characters until predicate evaluates to true while doing the following:
// - replacing XML character entity references with proper characters (&apos; &amp; &quot; &lt; &gt; &#...;)
// - condensing whitespace sequences to single space character
inline CDoc::TIterator CDoc::SkipAndExpand(TIterator &pI,const unsigned char *pTable,const unsigned char *pTablePure)
{
    //If entity translation, whitespace condense and whitespace trimming is disabled, use plain skip
    if (mParseFlags & EParseFlags::NO_ENTITY_TRANSLATION && 
        !(mParseFlags & EParseFlags::NORMALIZE_WHITESPACE) &&
        !(mParseFlags & EParseFlags::TRIM_WHITESPACE))
    {
        Skip(pI,pTable);
        return pI;
    }
    
    //Use simple skip until first modification is detected
    Skip(pI,pTablePure);

    //Use translation skip
    TIterator lSrc=pI;
    TIterator lDst=lSrc;

    while(Test(*lSrc,pTable))
    {
        // If entity translation is enabled    
        if(!(mParseFlags & EParseFlags::NO_ENTITY_TRANSLATION))
        {
            // Test if replacement is needed
            if(*lSrc=='&')
            {
                switch (lSrc[1])
                {
                // &amp; &apos;
                case 'a': 
                    if(lSrc[2]=='m'&&lSrc[3]=='p'&&lSrc[4]==';')
                    {
                        *lDst='&';
                        ++lDst;
                        lSrc+=5;
                        continue;
                    }
                    if(lSrc[2]=='p'&&lSrc[3]=='o'&&lSrc[4]=='s'&&lSrc[5]==';')
                    {
                        *lDst='\'';
                        ++lDst;
                        lSrc+=6;
                        continue;
                    }
                    break;

                // &quot;
                case 'q': 
                    if(lSrc[2]=='u'&&lSrc[3]=='o'&&lSrc[4]=='t'&&lSrc[5]==';')
                    {
                        *lDst='"';
                        ++lDst;
                        lSrc+=6;
                        continue;
                    }
                    break;

                // &gt;
                case 'g': 
                    if(lSrc[2]=='t'&&lSrc[3]==';')
                    {
                        *lDst='>';
                        ++lDst;
                        lSrc+=4;
                        continue;
                    }
                    break;

                // &lt;
                case 'l': 
                    if(lSrc[2]=='t'&&lSrc[3]==';')
                    {
                        *lDst='<';
                        ++lDst;
                        lSrc+=4;
                        continue;
                    }
                    break;

                // &#...; - assumes ASCII
                case '#':
                    {
                        unsigned long lCode = 0;
                        
                        if(lSrc[2]=='x')
                        {
                            lSrc+=3;   // Skip &#x
                            while(1)
                            {
                                unsigned char lDigit=lookup::cDigits[static_cast<unsigned char>(*lSrc)];
                                if(lDigit==0xFF)
                                    break;
                                lCode=16*lCode+lDigit;
                                ++lSrc;
                            }
                        }
                        else
                        {
                            lSrc+=2;   // Skip &#
                            while (1)
                            {
                                unsigned char lDigit=lookup::cDigits[static_cast<unsigned char>(*lSrc)];
                                if(lDigit==0xFF)
                                    break;
                                lCode=10*lCode+lDigit;
                                ++lSrc;
                            }
                        }
                        InsertCodedCharacter(lDst,lCode);    // Put character in output
                        Expect(lSrc,';');
                        continue;
                    }

                // Something else
                default:
                    // Ignore, just copy '&' verbatim
                    break;

                }
            }
        }
        
        // If whitespace condensing is enabled
        if(mParseFlags & EParseFlags::NORMALIZE_WHITESPACE)
        {
            // Test if condensing is needed                 
            if(Test(*lSrc,lookup::cWhitespace))
            {
                *lDst=' ';
                ++lDst;    // Put single space in dest
                ++lSrc;                      // Skip first whitespace char
                // Skip remaining whitespace chars
                Skip(lSrc,lookup::cWhitespace);
                continue;
            }
        }

        // No replacement, only copy character
        *lDst++ = *lSrc++;

    }

    // Return new end
    pI=lSrc;
    return lDst;
}

///////////////////////////////////////////////////////////////////////
// Internal parsing functions

// Parse BOM, if any
inline void CDoc::ParseBOM(TIterator &pI)
{
    // UTF-8?
    if (static_cast<unsigned char>(pI[0]) == 0xEF && 
        static_cast<unsigned char>(pI[1]) == 0xBB && 
        static_cast<unsigned char>(pI[2]) == 0xBF)
    {
        pI+=3;      // Skup utf-8 bom
    }
}

// Parse XML declaration (<?xml...)
inline CDoc::SNode *CDoc::ParseXMLDeclaration(TIterator &pI)
{
    // Create declaration
    SNode *lDeclaration = new(mPool) SNode(this,ENodeType::DECLARATION);

    // Skip whitespace before attributes or ?>
    Skip(pI,lookup::cWhitespace);

    // Parse declaration attributes
    ParseNodeAttributes(pI,lDeclaration);
    
    // Skip ?>
    Expect2(pI,'?','>');
    
    return lDeclaration;
}

// Parse XML comment (<!--...)
inline CDoc::SNode *CDoc::ParseComment(TIterator &pI)
{
    TIterator lB=pI;
    
    // Skip until end of cdata
    SkipUntil(pI,'-','-','>');
    
    TIterator lE=pI;
    pI+=3;
    
    if(!(mParseFlags & EParseFlags::COMMENT))
        return nullptr;       // Do not produce CDATA node

    // Create new cdata node
    return new(mPool) SNode(this,ENodeType::COMMENT,{},{lB,lE});
}

// Parse DOCTYPE
inline CDoc::SNode *CDoc::ParseDocType(TIterator &pI)
{
    // Remember value start
    TIterator lB=pI;

    // Skip to >
    while (*pI!='>')
    {
        // Determine character type
        switch (*pI)
        {
        
        // If '[' encountered, scan for matching ending ']' using naive algorithm with depth
        // This works for all W3C test files except for 2 most wicked
        case '[':
            {
                ++pI;     // Skip '['
                int lDepth = 1;
                while (lDepth > 0)
                {
                    switch(*pI)
                    {
                        case '[': ++lDepth; break;
                        case ']': --lDepth; break;
                        case 0: throw XParse("unexpected end of data", pI);
                    }
                    ++pI;
                }
                break;
            }
        
        // Error on end of text
        case '\0':
            throw XParse("unexpected end of data",pI);
        
        // Other character, skip it
        default:
            ++pI;

        }
    }
    
    // Create a new doctype node
    SNode *lDocType=new(mPool) SNode(this,ENodeType::DOCTYPE,{},{lB,pI});
    ++pI;      // skip '>'
    return lDocType;
}

// Parse PI
inline CDoc::SNode *CDoc::ParsePI(TIterator &pI)
{
    // If creation of PI nodes is enabled
    if(mParseFlags & EParseFlags::PI)
    {
        // Extract PI target name
        TIterator lB=pI;
        Expect(pI,lookup::cNodeName);
        Skip(pI,lookup::cNodeName);
        string_view lName(lB,pI);
        
        // Skip whitespace between pi target and pi
        Skip(pI,lookup::cWhitespace);

        // Remember start of pi
        lB=pI;
        
        // Skip to '?>'
        SkipUntil(pI,'?','>');

        // Create pi node
        SNode *lPI=new(mPool) SNode(this,ENodeType::PI,lName,{lB,pI});
        pI+=2;                          // Skip '?>'
        return lPI;
    }
    else
    {
        // Skip to '?>'
        SkipUntil(pI,'?','>');
        pI+=2;    // Skip '?>'
        return nullptr;
    }
}

// Parse and append data
inline void CDoc::ParseAndAppendData(TIterator &pI,SNode *pNode,TIterator pStart)
{
    //Backup to contents start if whitespace trimming is disabled
    if (!(mParseFlags & EParseFlags::TRIM_WHITESPACE))
        pI=pStart;     
    
    //Skip until end of data
    TIterator lB=pI,lE;
    if(mParseFlags & EParseFlags::NORMALIZE_WHITESPACE)
        lE=SkipAndExpand(pI,lookup::cText,lookup::cTextPureWS);   
    else
        lE=SkipAndExpand(pI,lookup::cText,lookup::cTextPureNoWS);   

    //Trim trailing whitespace if flag is set; leading was already trimmed by whitespace skip after >
    if(mParseFlags & EParseFlags::TRIM_WHITESPACE)
    {
        if (mParseFlags & EParseFlags::NORMALIZE_WHITESPACE)
        {
            // Whitespace is already condensed to single space characters by skipping function, so just trim 1 char off the end
            if (*(lE-1)==' ')
                --lE;
        }
        else
        {
            // Backup until non-whitespace character is found
            while(Test(*(lE-1),lookup::cWhitespace))
                --lE;
        }
    }
    
    // If characters are still left between end and value (this test is only necessary if normalization is enabled)
    // Create new data node
    if(lB!=lE&&!(mParseFlags & EParseFlags::NO_DATA_NODES))
    {
        SNode *lNode=new(mPool) SNode(this,ENodeType::DATA,{},{lB,lE});
        pNode->mChildren.push_back(*lNode);
    }
}

// Parse CDATA
inline CDoc::SNode *CDoc::ParseCDATA(TIterator &pI)
{
    TIterator lB=pI;
    
    // Skip until end of cdata
    SkipUntil(pI,']',']','>');
    
    TIterator lE=pI;
    pI+=3;
    
    if(mParseFlags & EParseFlags::NO_DATA_NODES)
        return nullptr;       // Do not produce CDATA node

    // Create new cdata node
    return new(mPool) SNode(this,ENodeType::CDATA,{},{lB,lE});
}

// Parse element node
inline CDoc::SNode *CDoc::ParseElement(TIterator &pI)
{
    // Extract element name
    TIterator lB=pI;
    Expect(pI,lookup::cNodeName);
    Skip(pI,lookup::cNodeName);
    
    SNode *lElement=new(mPool) SNode(this,ENodeType::ELEMENT,{lB,pI});
    
    // Skip whitespace between element name and attributes or >
    Skip(pI,lookup::cWhitespace);

    // Parse attributes, if any
    ParseNodeAttributes(pI,lElement);

    // Determine ending type
    if(*pI=='>')
    {
        ++pI;
        ParseNodeContents(pI,lElement);
    }
    else if(*pI=='/')
    {
        ++pI;
        Expect(pI,'>');
    }
    else
    {
        throw XParse("expected >",pI);
    }

    // Return parsed element
    return lElement;
}

// Determine node type, and parse it
// < is already skipped here
inline CDoc::SNode *CDoc::ParseNode(TIterator &pI)
{
    // Parse proper node type
    switch(*pI)
    {

    // <...
    default: 
        // Parse and append element node
        return ParseElement(pI);

    // <?...
    case '?': 
        ++pI;     // Skip ?
        if ((pI[0]=='x' || pI[0]=='X') &&
            (pI[1]=='m' || pI[1]=='M') && 
            (pI[2]=='l' || pI[2]=='L') &&
            Test(pI[3],lookup::cWhitespace))
        {
            // '<?xml ' - xml declaration
            pI+=4;
            return ParseXMLDeclaration(pI);
        }
        else
        {
            // Parse PI
            return ParsePI(pI);
        }
    
    // <!...
    case '!':
        // Parse proper subset of <! node
        switch (pI[1])    
        {
        
        // <!-
        case '-':
            if (pI[2]=='-')
            {
                // '<!--' - xml comment
                pI+=3;     // Skip '!--'
                return ParseComment(pI);
            }
            break;

        // <![
        case '[':
            if (pI[2]=='C'&&pI[3]=='D'&&pI[4]=='A' && pI[5]=='T'&&pI[6]=='A'&&pI[7]=='[')
            {
                // '<![CDATA[' - cdata
                pI+=8;     // Skip '![CDATA['
                return ParseCDATA(pI);
            }
            break;

        // <!D
        case 'D':
            if (pI[2]=='O'&&pI[3]=='C'&&pI[4]=='T' && pI[5]=='Y'&&pI[6]=='P'&&pI[7]=='E'&&Test(pI[8],lookup::cWhitespace))
            {
                // '<!DOCTYPE ' - doctype
                pI+=9;      // skip '!DOCTYPE '
                return ParseDocType(pI);
            }

        }   // switch

        // Attempt to skip other, unrecognized node types starting with <!
        ++pI;     // Skip !
        SkipUntil(pI,'>');
        ++pI;     // Skip '>'
        return nullptr;   // No node recognized
    }
}

// Parse contents of the node - children, data etc.
inline void CDoc::ParseNodeContents(TIterator &pI,SNode *pNode)
{
    TIterator lContentsStart=pI;      // Store start of node contents before whitespace is skipped
    Skip(pI,lookup::cWhitespace);

    // For all children and text
    while(true)
    {
        // Skip whitespace between > and node contents
        
        // Determine what comes next: node closing, child node, data node, or 0?
        switch(*pI)
        {
        // Node closing or child node
        case '<':
            if(pI[1]=='/')
            {
                // Node closing
                pI+=2;      // Skip '</'
                if(mParseFlags&EParseFlags::VALIDATE_TAGS)
                {
                    // Skip and validate closing tag name
                    TIterator lB=pI;
                    Skip(pI,lookup::cNodeName);
                    if(string_view(lB,pI)!=pNode->mName)
                        throw XParse("invalid closing tag name",pI);
                }
                else
                {
                    // No validation, just skip name
                    Skip(pI,lookup::cNodeName);
                }
                // Skip remaining whitespace after node name
                Skip(pI,lookup::cWhitespace);
                Expect(pI,'>');
                return;     // Node closed, finished parsing contents
            }
            else
            {
                // Child node
                ++pI;     // Skip '<'
                if(SNode *lChild=ParseNode(pI))
                    pNode->mChildren.push_back(*lChild);
            }
            lContentsStart=pI;
            Skip(pI,lookup::cWhitespace);
            break;

        // End of data - error
        case '\0':
            throw XParse("unexpected end of data",pI);

        // Data node
        default:
            ParseAndAppendData(pI,pNode,lContentsStart);
            break;
        }
    }
}

// Parse XML attributes of the node
inline void CDoc::ParseNodeAttributes(TIterator &pI,SNode *pNode)
{
    // For all attributes 
    while(Test(*pI,lookup::cAttributeName))
    {
        // Extract attribute name
        TIterator lB=pI,lE;
        Skip(++pI,lookup::cAttributeName);

        // Create new attribute
        string_view lName(lB,pI);

        // Skip whitespace after attribute name
        Skip(pI,lookup::cWhitespace);
        Expect(pI,'=');

        // Skip whitespace after =
        Skip(pI,lookup::cWhitespace);

        // Skip quote and remember if it was ' or "
        char lQuote=ExpectOneOf(pI,'\'','"');

        // Extract attribute value and expand char refs in it
        lB=pI;
        
        EParseFlags lParseFlags=mParseFlags;
        mParseFlags&=~EParseFlags::NORMALIZE_WHITESPACE;
        if (lQuote=='\'')
            lE=SkipAndExpand(pI,lookup::cAttributeData1,lookup::cAttributeData1Pure);
        else
            lE=SkipAndExpand(pI,lookup::cAttributeData2,lookup::cAttributeData2Pure);
        mParseFlags=lParseFlags;

        SAttribute *lAttr=new(mPool) SAttribute(lName,{lB,lE});
        pNode->mAttributes.push_front(*lAttr);
        
        Expect(pI,lQuote);
        
        Skip(pI,lookup::cWhitespace);
    }
}


void CDoc::DoParse()
{
    Clear();
    
    auto lI=mText.begin();
    
    // Parse BOM, if any
    ParseBOM(lI);
    
    // Parse children
    while (1)
    {
        // Skip whitespace before node
        Skip(lI,lookup::cWhitespace);
        if(*lI==0)
            break;

        // Parse and append new child
        if(*lI=='<')
        {
            ++lI;     // Skip '<'
            if(SNode *lNode=ParseNode(lI))
                mDocNode->mChildren.push_back(*lNode);
        }
        else
        {
            throw XParse("expected <",lI);
        }
    }
}

CNode CDoc::DocNode()
{
    return mDocNode;
}

CNode CDoc::Root()
{
    if(!mDocNode) return nullptr;
    
    for(auto &c : mDocNode->mChildren)
        if(c.mType==ENodeType::ELEMENT)
            return &c;
    return nullptr;
}

CNode CDoc::NewNode(const std::string &pName,const std::string &pValue)
{
    return CreateNode(ENodeType::ELEMENT,pName,pValue);
}

/*namespace xml*/ } /*namespace buola*/ }
